//Licensed under Apache License version 2.0
package javax.jmdns.impl.tasks.state;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.jmdns.ServiceInfo;
import javax.jmdns.impl.DNSOutgoing;
import javax.jmdns.impl.DNSStatefulObject;
import javax.jmdns.impl.JmDNSImpl;
import javax.jmdns.impl.ServiceInfoImpl;
import javax.jmdns.impl.constants.DNSConstants;
import javax.jmdns.impl.constants.DNSState;
import javax.jmdns.impl.tasks.DNSTask;

/**
 * This is the root class for all state tasks. These tasks work with objects that implements the {@link javax.jmdns.impl.DNSStatefulObject} interface and therefore participate in the state machine.
 *
 * @version %I%, %G%
 * @author Pierre Frisch
 */
public abstract class DNSStateTask extends DNSTask
{
    static Logger logger1 = Logger.getLogger(DNSStateTask.class.getName());

    /**
     * By setting a 0 ttl we effectively expire the record.
     */
    private final int _ttl;

    private static int _defaultTTL = DNSConstants.DNS_TTL;

    /**
     * The state of the task.
     */
    private DNSState _taskState = null;

    public abstract String getTaskDescription();

    public static int defaultTTL()
    {
        return _defaultTTL;
    }

    /**
     * For testing only do not use in production.
     *
     * @param value
     */
    public static void setDefaultTTL(int value)
    {
        _defaultTTL = value;
    }

    /**
     * @param jmDNSImpl
     * @param ttl
     */
    public DNSStateTask(JmDNSImpl jmDNSImpl, int ttl)
    {
        super(jmDNSImpl);
        _ttl = ttl;
    }

    /**
     * @return the ttl
     */
    public int getTTL()
    {
        return _ttl;
    }

    /**
     * Associate the DNS host and the service infos with this task if not already associated and in the same state.
     *
     * @param state
     *            target state
     */
    protected void associate(DNSState state)
    {
        synchronized (this.getDns())
        {
            this.getDns().associateWithTask(this, state);
        }
        for (ServiceInfo serviceInfo : this.getDns().getServices().values())
        {
            ((ServiceInfoImpl) serviceInfo).associateWithTask(this, state);
        }
    }

    /**
     * Remove the DNS host and service info association with this task.
     */
    protected void removeAssociation()
    {
        // Remove association from host to this
        synchronized (this.getDns())
        {
            this.getDns().removeAssociationWithTask(this);
        }

        // Remove associations from services to this
        for (ServiceInfo serviceInfo : this.getDns().getServices().values())
        {
            ((ServiceInfoImpl) serviceInfo).removeAssociationWithTask(this);
        }
    }

    @Override
    public void run()
    {
        DNSOutgoing out = new DNSOutgoing(DNSConstants.FLAGS_QR_QUERY);
        try
        {
            if (!this.checkRunCondition())
            {
                this.cancel();
                return;
            }
            List<DNSStatefulObject> stateObjects = new ArrayList<DNSStatefulObject>();
            // send probes for JmDNS itself
            synchronized (this.getDns())
            {
                if (this.getDns().isAssociatedWithTask(this, this.getTaskState()))
                {
                    logger1.finer(this.getName() + ".run() JmDNS " + this.getTaskDescription() + " " + this.getDns().getName());
                    stateObjects.add(this.getDns());
                    out = this.buildOutgoingForDNS(out);
                }
            }
            // send probes for services
            for (ServiceInfo serviceInfo : this.getDns().getServices().values())
            {
                ServiceInfoImpl info = (ServiceInfoImpl) serviceInfo;

                synchronized (info)
                {
                    if (info.isAssociatedWithTask(this, this.getTaskState()))
                    {
                        logger1.fine(this.getName() + ".run() JmDNS " + this.getTaskDescription() + " " + info.getQualifiedName());
                        stateObjects.add(info);
                        out = this.buildOutgoingForInfo(info, out);
                    }
                }
            }
            if (!out.isEmpty())
            {
                logger1.finer(this.getName() + ".run() JmDNS " + this.getTaskDescription() + " #" + this.getTaskState());
                this.getDns().send(out);

                // Advance the state of objects.
                this.advanceObjectsState(stateObjects);
            }
            else
            {
                // Advance the state of objects.
                this.advanceObjectsState(stateObjects);

                // If we have nothing to send, another timer taskState ahead of us has done the job for us. We can cancel.
                cancel();
                return;
            }
        }
        catch (Throwable e)
        {
            logger1.log(Level.WARNING, this.getName() + ".run() exception ", e);
            this.recoverTask(e);
        }

        this.advanceTask();
    }

    protected abstract boolean checkRunCondition();

    protected abstract DNSOutgoing buildOutgoingForDNS(DNSOutgoing out) throws IOException;

    protected abstract DNSOutgoing buildOutgoingForInfo(ServiceInfoImpl info, DNSOutgoing out) throws IOException;

    protected void advanceObjectsState(List<DNSStatefulObject> list)
    {
        if (list != null)
        {
            for (DNSStatefulObject object : list)
            {
                synchronized (object)
                {
                    object.advanceState(this);
                }
            }
        }
    }

    protected abstract void recoverTask(Throwable e);

    protected abstract void advanceTask();

    /**
     * @return the taskState
     */
    protected DNSState getTaskState()
    {
        return this._taskState;
    }

    /**
     * @param taskState
     *            the taskState to set
     */
    protected void setTaskState(DNSState taskState)
    {
        this._taskState = taskState;
    }

}
